package com.garbagecode.util.pagination;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.ResultMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import com.garbagecode.util.pagination.dialect.Dialect;


/**
 * 
 * @author cc.z
 *
 */
@Intercepts({ @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,
    	RowBounds.class, ResultHandler.class }) })
public class ResultBoundsInterceptor implements Interceptor {
	private static List<ResultMap> totalResultMaps;
	private static Dialect dialect;
	public static final String PROPERTY_DIALECT_NAME = "dialect";
	public static final Object key = new Object();

	static {
		init();
	}

	private static void init() {
		String id = ResultBoundsInterceptor.class.getName() + "." + Long.class.getName();
		Class<?> type = Long.class;
		ArrayList<ResultMapping> resultMappings = new ArrayList<ResultMapping>();

		ResultMap.Builder resultMapBuilder = new ResultMap.Builder(null, id, type, resultMappings);
		ResultMap newResultMap = resultMapBuilder.build();

		totalResultMaps = new ArrayList<ResultMap>();
		totalResultMaps.add(newResultMap);
		totalResultMaps = Collections.unmodifiableList(totalResultMaps);
	}

	@SuppressWarnings("rawtypes")
	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		Executor executor = (Executor) invocation.getTarget();
		Object[] args = invocation.getArgs();
		MappedStatement ms = (MappedStatement) args[0];
		Object parameter = args[1];
		RowBounds rowBounds = (RowBounds) args[2];

		if (rowBounds instanceof ResultBounds.Total) {
			BoundSql boundSql = ms.getBoundSql(parameter);
			String newSql = dialect.getTotalSql(boundSql.getSql());
			SqlSource newSqlSource = new ResultBoundSqlSource(ms.getConfiguration(), newSql,
					boundSql.getParameterMappings(), boundSql.getParameterObject());

			MappedStatement newMs = ResultBoundsUtils.newMappedStatement(ms, newSqlSource, totalResultMaps);

			// obtain total of rows
			return executor.query(newMs, parameter, RowBounds.DEFAULT, (ResultHandler) args[3]);
		} else {
			ResultBounds resultBounds = ResultBounds.wrap((RowBounds) rowBounds);

			// set total
			if (rowBounds instanceof ResultBounds) {
				resultBounds.setTotal(new ResultBounds.Total(ms.getId(), parameter));
			}

			if (resultBounds != null && resultBounds.hasBounds()) {
				BoundSql boundSql = ms.getBoundSql(parameter);

				String newSql = dialect.getBoundSql(boundSql.getSql(), resultBounds.getOffset(),
						resultBounds.getLimit(), resultBounds.getOrder());

				SqlSource newSqlSource = new ResultBoundSqlSource(ms.getConfiguration(), newSql,
						boundSql.getParameterMappings(), boundSql.getParameterObject());

				MappedStatement newMs = ResultBoundsUtils.newMappedStatement(ms, newSqlSource, null);

				// pagination
				return executor.query(newMs, parameter, RowBounds.DEFAULT, (ResultHandler) args[3]);
			}
		}

		return invocation.proceed();
	}

	@Override
	public Object plugin(Object target) {
		return Plugin.wrap(target, this);
	}
	
	/**
	 * call once
	 */
	@Override
	public void setProperties(Properties properties) {
		if (dialect != null) {
			return;
		}
		
		// 无论 setProperties 被调用多少次，保证 dialect 只会被初始化一次
		synchronized(key) {
			if (dialect != null) {
				return;
			}
			
			String dialectName = properties.getProperty(PROPERTY_DIALECT_NAME);
	
			if (dialectName == null || dialectName.trim().isEmpty()) {
				throw new RuntimeException("property '" + PROPERTY_DIALECT_NAME + "' is required");
			}
	
			Class<?> cls;
			try {
				cls = Class.forName(dialectName);
			} catch (ClassNotFoundException e) {
				throw new RuntimeException("not found class '" + dialectName + "'");
			}
	
			Object newObj;
			try {
				newObj = cls.newInstance();
			} catch (Exception e) {
				throw new RuntimeException("create instance of class '" + cls.getName() + "' fail");
			}
	
			if (newObj instanceof Dialect) {
				dialect = (Dialect) newObj;
			} else {
				throw new RuntimeException(String.format("class '%s' is not subclass of class '%s'", 
					newObj.getClass().getName(), Dialect.class.getName()));
			}
			
			// set dialect
			dialect = (Dialect) newObj;
		}
	}
}